﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace ProceduralGolf
{
    public abstract class Camera
    {
        public float fov = MathHelper.ToRadians(45);
        public float aspectRatio = 4.0f / 3.0f;
        public float nearPlane = 0.1f;
        public float farPlane = 10000.0f;
        abstract public Matrix GetViewMatrix();
        public Matrix GetProjectionMatrix()
        {
            return Matrix.CreatePerspectiveFieldOfView(fov, aspectRatio, nearPlane, farPlane);
        }
        abstract public Vector3 GetViewVector();

        abstract public Vector3 GetPosition();
    }

    #region AimCam
    class AimCam : Camera
    {
        const float rotationSpeed = 0.01f;
        Vector3 positionOffset = new Vector3(0.0f, 20.0f, 50.0f);
        Vector3 lookatOffset = new Vector3(0.0f, 10.0f, 0.0f);

        public Vector3 Position;
        
        public float angle;

        Vector3 camPosition;
        Vector3 lookatPosition;

        public AimCam()
        {
            Position = Vector3.Zero;
        }

        public void SetTarget(Vector3 target)
        {
            // calc angle from position to target
            Vector3 direction = target - Position;
            angle = (float)(Math.Atan2(direction.Z, direction.X)) + MathHelper.PiOver2;
        }

        void Update()
        {
            Matrix transform = Matrix.Identity;
            transform.Up = Vector3.Up;
            transform.Right = Vector3.Cross(Vector3.Up, Vector3.UnitZ);
            transform *= Matrix.CreateRotationY(-angle);

            camPosition = Position + Vector3.TransformNormal(positionOffset, transform);
            lookatPosition = Position + Vector3.TransformNormal(lookatOffset, transform);
        }

        public override Matrix GetViewMatrix()
        {
            return Matrix.CreateLookAt(camPosition, lookatPosition, Vector3.Up);
        }

        public override Vector3 GetViewVector()
        {
            return lookatPosition - camPosition;
        }

        public override Vector3 GetPosition()
        {
            return camPosition;
        }

        public void Update(GamePadState state)
        {
            angle += state.ThumbSticks.Right.X * rotationSpeed;
            Update();
        }

#if !XBOX
        public void Update(KeyboardState state)
        {
            if (state.IsKeyDown(Keys.Left))
                angle -= rotationSpeed;
            if (state.IsKeyDown(Keys.Right))
                angle += rotationSpeed;

            Update();
        }
#endif
    }
    #endregion
    #region ViewCam
    class ViewCam
    {
        const float moveSpeed = 0.1f;

        public Vector3 Position;
        public Vector3 Target;

        public ViewCam(float height)
        {
            Position = Vector3.Up * height;
            Target = Vector3.Forward;
        }

        public Matrix GetViewMatrix()
        {
            return Matrix.CreateLookAt(Position, Target, Vector3.Up);
        }

        public Vector3 GetViewVector()
        {
            return Target - Position;
        }

        public void Update(GamePadState state)
        {
            Position.X += state.ThumbSticks.Right.X * moveSpeed;
            Position.Z += state.ThumbSticks.Right.Y * moveSpeed;
        }

#if !XBOX
        public void Update(KeyboardState state)
        {
            if (state.IsKeyDown(Keys.Left))
                Position.X -= moveSpeed;
            if (state.IsKeyDown(Keys.Right))
                Position.X += moveSpeed;
            if (state.IsKeyDown(Keys.Up))
                Position.Z -= moveSpeed;
            if (state.IsKeyDown(Keys.Down))
                Position.Z += moveSpeed;
        }
#endif
    }
    #endregion
    #region ChaseCam
    public class ChaseCamera : Camera
    {
        float yaw = 0.0f;
        float pitch = 0.0f;

        #region Chased object properties (set externally each frame)

        /// <summary>
        /// Position of object being chased.
        /// </summary>
        public Vector3 ChasePosition
        {
            get { return chasePosition; }
            set { chasePosition = value; }
        }
        private Vector3 chasePosition;

        /// <summary>
        /// Direction the chased object is facing.
        /// </summary>
        public Vector3 ChaseDirection
        {
            get { return chaseDirection; }
            set { chaseDirection = value; }
        }
        private Vector3 chaseDirection;

        /// <summary>
        /// Chased object's Up vector.
        /// </summary>
        public Vector3 Up
        {
            get { return up; }
            set { up = value; }
        }
        private Vector3 up = Vector3.Up;

        #endregion

        #region Desired camera positioning (set when creating camera or changing view)

        /// <summary>
        /// Desired camera position in the chased object's coordinate system.
        /// </summary>
        public Vector3 DesiredPositionOffset
        {
            get { return desiredPositionOffset; }
            set { desiredPositionOffset = value; }
        }
        private Vector3 desiredPositionOffset = new Vector3(0.0f, 20.0f, 50.0f);

        /// <summary>
        /// Desired camera position in world space.
        /// </summary>
        public Vector3 DesiredPosition
        {
            get
            {
                // Ensure correct value even if update has not been called this frame
                UpdateWorldPositions();

                return desiredPosition;
            }
        }
        private Vector3 desiredPosition;

        /// <summary>
        /// Look at point in the chased object's coordinate system.
        /// </summary>
        public Vector3 LookAtOffset
        {
            get { return lookAtOffset; }
            set { lookAtOffset = value; }
        }
        private Vector3 lookAtOffset = new Vector3(0, 10.0f, 0);

        /// <summary>
        /// Look at point in world space.
        /// </summary>
        public Vector3 LookAt
        {
            get
            {
                // Ensure correct value even if update has not been called this frame
                UpdateWorldPositions();

                return lookAt;
            }
        }
        private Vector3 lookAt;

        #endregion

        #region Camera physics (typically set when creating camera)

        /// <summary>
        /// Physics coefficient which controls the influence of the camera's position
        /// over the spring force. The stiffer the spring, the closer it will stay to
        /// the chased object.
        /// </summary>
        public float Stiffness
        {
            get { return stiffness; }
            set { stiffness = value; }
        }
        private float stiffness = 1800.0f;

        /// <summary>
        /// Physics coefficient which approximates internal friction of the spring.
        /// Sufficient damping will prevent the spring from oscillating infinitely.
        /// </summary>
        public float Damping
        {
            get { return damping; }
            set { damping = value; }
        }
        private float damping = 600.0f;

        /// <summary>
        /// Mass of the camera body. Heaver objects require stiffer springs with less
        /// damping to move at the same rate as lighter objects.
        /// </summary>
        public float Mass
        {
            get { return mass; }
            set { mass = value; }
        }
        private float mass = 50.0f;

        #endregion

        #region Current camera properties (updated by camera physics)

        public override Vector3 GetPosition()
        {
            return Position;
        }
        /// <summary>
        /// Position of camera in world space.
        /// </summary>
        public Vector3 Position
        {
            get { return position; }
        }
        private Vector3 position;

        /// <summary>
        /// Velocity of camera.
        /// </summary>
        public Vector3 Velocity
        {
            get { return velocity; }
        }
        private Vector3 velocity;

        #endregion

        #region Perspective properties

        /// <summary>
        /// Perspective aspect ratio. Default value should be overriden by application.
        /// </summary>
        public float AspectRatio
        {
            get { return aspectRatio; }
            set { aspectRatio = value; }
        }

        /// <summary>
        /// Perspective field of view.
        /// </summary>
        public float FieldOfView
        {
            get { return fieldOfView; }
            set { fieldOfView = value; }
        }
        private float fieldOfView = MathHelper.ToRadians(45.0f);

        /// <summary>
        /// Distance to the near clipping plane.
        /// </summary>
        public float NearPlaneDistance
        {
            get { return nearPlaneDistance; }
            set { nearPlaneDistance = value; }
        }
        private float nearPlaneDistance = 1.0f;

        /// <summary>
        /// Distance to the far clipping plane.
        /// </summary>
        public float FarPlaneDistance
        {
            get { return farPlaneDistance; }
            set { farPlaneDistance = value; }
        }
        private float farPlaneDistance = 10000.0f;

        #endregion

        #region Matrix properties

        /// <summary>
        /// View transform matrix.
        /// </summary>
        public Matrix View
        {
            get { return view; }
        }
        private Matrix view;

        /// <summary>
        /// Projecton transform matrix.
        /// </summary>
        public Matrix Projection
        {
            get { return projection; }
        }
        private Matrix projection;

        #endregion

        #region Methods

        /// <summary>
        /// Rebuilds object space values in world space. Invoke before publicly
        /// returning or privately accessing world space values.
        /// </summary>
        private void UpdateWorldPositions()
        {
            // Construct a matrix to transform from object space to worldspace
            Matrix transform = Matrix.Identity;
            transform.Forward = ChaseDirection;
            transform.Up = Up;
            transform.Right = Vector3.Cross(Up, ChaseDirection);

            transform *= Matrix.CreateRotationX(pitch) * Matrix.CreateRotationY(-yaw);
            // Calculate desired camera properties in world space
            desiredPosition = ChasePosition + Vector3.TransformNormal(DesiredPositionOffset, transform);
            lookAt = ChasePosition + Vector3.TransformNormal(LookAtOffset, transform);
        }

        /// <summary>
        /// Rebuilds camera's view and projection matricies.
        /// </summary>
        private void UpdateMatrices()
        {
            Vector3 newPosition = this.Position + Vector3.Transform(Vector3.UnitZ, Matrix.CreateRotationX(pitch) * Matrix.CreateRotationY(yaw));

            view = Matrix.CreateLookAt(this.Position, this.LookAt, this.Up);
            projection = Matrix.CreatePerspectiveFieldOfView(FieldOfView,
                AspectRatio, NearPlaneDistance, FarPlaneDistance);
        }

        /// <summary>
        /// Forces camera to be at desired position and to stop moving. The is useful
        /// when the chased object is first created or after it has been teleported.
        /// Failing to call this after a large change to the chased object's position
        /// will result in the camera quickly flying across the world.
        /// </summary>
        public void Reset()
        {
            UpdateWorldPositions();

            // Stop motion
            velocity = Vector3.Zero;

            // Force desired position
            position = desiredPosition;

            UpdateMatrices();
        }

        /// <summary>
        /// Animates the camera from its current position towards the desired offset
        /// behind the chased object. The camera's animation is controlled by a simple
        /// physical spring attached to the camera and anchored to the desired position.
        /// </summary>
        public void Update()
        {

            UpdateWorldPositions();

            float elapsed = 1.0f / 100.0f;// (float)gameTime.ElapsedGameTime.TotalSeconds;

            // Calculate spring force
            Vector3 stretch = position - desiredPosition;
            Vector3 force = -stiffness * stretch - damping * velocity;

            // Apply acceleration
            Vector3 acceleration = force / mass;
            velocity += acceleration * elapsed;

            // Apply velocity
            position += velocity * elapsed;

            UpdateMatrices();
        }

        #endregion

        public override Matrix GetViewMatrix()
        {
            return View;
        }

        public override Vector3 GetViewVector()
        {
            return Vector3.Transform(LookAt - Position, Matrix.CreateRotationY(0));
        }

        public void Update(GamePadState gamepadState)
        {
            Update();
        }

#if !XBOX
        public void Update(KeyboardState keyboardState)
        {
            Update();
        }
#endif
    }
    #endregion
}
